import {
  ResultInfo,
  CacheConfig,
  ApiTrackPlugin,
  ApiTrackInitConfig,
  LifecycleHook,
  DataSet,
  ResultType,
  ApiTrackStatus,
  ApiTrackReporter,
  PresetSencePlugin,
  ErrorName,
  EventName,
} from "../types";
import { isEmpty } from "lodash-es";
import { ruleCompile } from "./compile";
import { validateHandler } from "./validate";
import { PresetSence, Rule, RuleGroupItem } from "../types/rule";
import { presetSencePluginMap } from "@/presets";
import { ErrorNameMap } from "@/constants";

type PluginItem = (ResultInfo?: ResultInfo) => void;

const STATUS_WEIGHT_MAP = {
  [ApiTrackStatus.UnReady]: 0,
  [ApiTrackStatus.Ready]: 1,
  [ApiTrackStatus.Start]: 2,
  [ApiTrackStatus.Running]: 3,
  [ApiTrackStatus.Finished]: 4,
};

export class ApiTrack {
  readonly biz_id: string | undefined;
  RulesMap: Map<string, Rule[]>;
  private context: Record<string, unknown>;
  private plugins: PluginItem[];
  private cacheList: DataSet[];
  private status = ApiTrackStatus.UnReady;
  readonly env: string;
  readonly reporter: ApiTrackReporter;
  private fetchRuleFlag: boolean;
  private cache?: CacheConfig;
  private presetSence: Record<string, PresetSencePlugin>;
  private createTiming: number;
  hooks: LifecycleHook;
  constructor(config: ApiTrackInitConfig) {
    this.biz_id = config.biz_id;
    this.cacheList = [];
    this.plugins = [];
    this.env = config.env || "other";
    this.createTiming = Date.now();
    this.RulesMap = new Map();
    this.reporter = config.reporter;
    // 需要先初始化勾走，不然后续流程失败无法上报
    this.hooks = {
      onJsError: (location, error, extra) => {
        const message = error instanceof Error ? error.message : String(error);
        this.reporter?.reportError?.(ErrorName.JsError, { location, message, ...extra });
        config.onJsError?.(ErrorNameMap[ErrorName.JsError], error);
        // console.error(error);
      },
      onReportResult: config.onReportResult,
      onCacheTimeOut: config.onCacheTimeOut,
      onApiTrackReady: config.onApiTrackReady,
    };
    this.context = {};
    this.setContext(config.context || {});
    // 默认开启缓存
    this.cache = {
      disabled: false,
      cacheCount: 5,
      timeout: 10000,
      ...config?.cache,
    };
    this.presetSence = {
      ...config?.presets,
      ...presetSencePluginMap,
    };

    this.fetchRuleFlag = false;

    this.fetchRuleGroup(config.fetchRuleFn);
    this.setStatus(this.cache.disabled ? ApiTrackStatus.Start : ApiTrackStatus.Ready);
    // 处理校验规则
    config.rules && this.addRules(config.rules);

    if (!this.cache.disabled) {
      // 超时释放缓存
      setTimeout(() => {
        // 只要存在缓存，超时就需要释放
        if (this.cacheList.length === 0) return;
        const cloneCacheList = JSON.parse(JSON.stringify(this.cacheList));
        this.hooks.onCacheTimeOut?.(cloneCacheList, this);
        // 如果超时，并且规则数量为0，直接close
        if (this.status !== ApiTrackStatus.Start || this.RulesMap.size === 0) {
          this.close();
        }
        this.reporter.sendLog(EventName.CacheTimeout);
        // console.warn('ApiTrack timed out!');
      }, this.cache.timeout);
    }
  }

  // 限定通过setStatus修改状态
  setStatus(nextStatus: ApiTrackStatus) {
    // 状态权重不可逆转
    if (STATUS_WEIGHT_MAP[this.status] >= STATUS_WEIGHT_MAP[nextStatus]) {
      // console.warn('The weight of next status is precede currnet.');
      return;
    }
    this.reporter.sendLog(EventName.StageTiming, {
      metrics: {
        [String(nextStatus)]: Date.now() - this.createTiming,
      },
    });
    this.status = nextStatus;
  }

  setContext(context: Record<string, unknown> = {}) {
    try {
      Object.assign(this.context, context);
      if (!isEmpty(this.context)) {
        // 更新上报信息
        this.reporter.setBaseInfo(this.context);
      }
    } catch (error) {
      this.hooks?.onJsError?.("SetContext", error);
    }
  }

  // 获取规则
  async fetchRuleGroup(fetchRuleFn?: (apiTrack?: ApiTrack) => Promise<RuleGroupItem[] | undefined>) {
    if (typeof fetchRuleFn !== "function") {
      this.fetchRuleFlag = true;
      return;
    }
    try {
      const ruleGroup = await fetchRuleFn?.(this);
      this.reporter.sendLog(EventName.FetchRuleSuccess, {
        metrics: {
          fetch_rule_timing: Date.now() - this.createTiming,
        },
      });
      if (ruleGroup) {
        this.addRules(ruleGroup);
        this.fetchRuleFlag = true;
        this.run();
        return;
      }
      throw new Error("The ruleGroup is invalid");
    } catch (error) {
      this.reporter.sendLog(EventName.FetchRuleFail, {
        categories: {
          error: error instanceof Error ? error.message : String(error),
        },
      });
    }
  }

  // Start 是手动就绪标识，需要等待规则获取完成
  async start() {
    this.setStatus(ApiTrackStatus.Start);
    this.run();
  }

  async run() {
    if (this.status !== ApiTrackStatus.Start || !this.fetchRuleFlag) {
      // console.warn(`WARN: When Status is Start and The fetchRuleFlag is true, the status can turn to Running!`);
      return;
    }
    this.setStatus(ApiTrackStatus.Running);
    while (this.cacheList.length) {
      const cache = this.cacheList.shift();
      if (!cache) break;
      await this.check(cache);
    }
    this.hooks.onApiTrackReady?.(this);
  }

  close() {
    this.cacheList = [];
    this.RulesMap.clear();
    this.setStatus(ApiTrackStatus.Finished);
  }

  async addRules(rules: Rule[]) {
    try {
      if (this.status === ApiTrackStatus.Finished || !rules.length) return;
      const start = Date.now();
      for (const rule of rules) {
        if (!/.*\/$/.test(rule.api_path)) {
          rule.api_path += "/";
        }
        const temp = this.RulesMap.get(rule.api_path) || [];
        const ruleCompiled = ruleCompile(rule, this);
        ruleCompiled && temp.push(ruleCompiled);
        this.RulesMap.set(rule.api_path, temp);
      }
      this.reporter.sendLog(EventName.CompileRuleTiming, {
        metrics: {
          timing: Date.now() - start,
        },
      });
    } catch (error) {
      this.hooks.onJsError?.("Add Rules", error);
    }
  }

  use<P>(fn: ApiTrackPlugin<ApiTrack, P>, options?: P): this {
    try {
      this.plugins.push(fn(this, options));
    } catch (error) {
      this.hooks.onJsError?.("Add plugin", error);
    }
    return this;
  }

  // 校验预设
  runPreset(presetSences: PresetSence[], dataSet: DataSet) {
    for (const { preset_id, options } of presetSences) {
      try {
        this.presetSence?.[preset_id]?.(this, dataSet, options);
      } catch (error) {
        this.hooks.onJsError?.("Run Preset", error, { preset_id, options });
      }
    }
  }

  async check(dataSet: DataSet) {
    try {
      const { api_path, log_id } = dataSet;
      dataSet.context = this.context;
      if (!this.RulesMap.has(api_path)) {
        return;
      }
      const curRules = this.RulesMap.get(api_path);
      if (!curRules) return [];
      curRules.forEach((rule) => {
        rule.preset_sences && this.runPreset(rule.preset_sences, dataSet);
        const validateResult = validateHandler(dataSet, rule);
        // 不满足Condition
        if (typeof validateResult === "string") return;
        // 校验有错误
        if (validateResult?.length) {
          validateResult.forEach((item) => {
            this.report(item);
          });
        } else {
          this.report({
            api_path,
            log_id,
            ...rule.info,
            type: ResultType.success,
          });
        }
      });
    } catch (error) {
      this.hooks.onJsError?.("Check DataSet", error);
    }
  }

  async track(dataSet: DataSet) {
    try {
      if (this.status === ApiTrackStatus.Finished || !dataSet.api_path) return;
      if (!/.*\/$/.test(dataSet.api_path)) {
        dataSet.api_path += "/";
      }
      if (
        [ApiTrackStatus.UnReady, ApiTrackStatus.Ready, ApiTrackStatus.Start].includes(this.status) &&
        this.cacheList.length < (this.cache?.cacheCount || 5)
      ) {
        this.cacheList.push(dataSet);
        return;
      }
      return this.check(dataSet);
    } catch (error) {
      this.hooks.onJsError?.("Track", error);
    }
  }

  report(result: ResultInfo) {
    try {
      this.plugins.forEach((plugin) => {
        plugin(result);
      });
      this.hooks.onReportResult?.(result);
    } catch (error) {
      this.hooks.onJsError?.("Report", error);
    }
  }
}
